/**
 * 
 */
package com.wgo.bpot.client.session;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;

import com.wgo.bpot.common.transport.exception.db.DatabaseException;
import com.wgo.bpot.common.transport.servicefacade.DispatchedModifyingMethodsTransport;
import com.wgo.bpot.common.transport.servicefacade.LightService;
import com.wgo.bpot.common.transport.servicefacade.ServiceFactory;
import com.wgo.bpot.common.transport.servicefacade.ServiceFactoryLocator;
import com.wgo.bpot.common.transport.util.DomainModelTypeNamingConvention;
import com.wgo.bpot.common.transport.util.MethodNamingConvention;



/**
 * @author petterei
 * @version $Id: LightSession.java,v 1.3 2006-01-27 07:47:44 oranvik Exp $
 */
public class LightSession implements Session {

	
	private static final Logger log = Logger.getLogger(LightSession.class.getSimpleName());
	
	public static final String SERVICE_SUB_URL = "/lightservice";
	public static final int DEFAULT_UPDATE_DEPTH = 1;
	public static final String CLIENT_VERSION = "0.2.1";

	private Map<Class, DomainProxy> rootSystems = null;
	private LightService service;
	private URL serverContextUrl;
	private boolean isAnyDirty = false;
	private int initLinkDepth = 1;
	private static Method getDbIdMethod;
	private Class rootConceptClass = null;
//	TODO  	The instantiating of the serviceFactory should be done through dependency injection
//	 		or a locator service.
	private static ServiceFactory serviceFactory = ServiceFactoryLocator.getServiceFactory();
//	private volatile boolean conceptVisitedMark = true;
	
	static {
		try {
			getDbIdMethod = MethodNamingConvention.PROXY_SUPER_INTERFACE.getMethod("getDbId", (Class[]) null);
		} catch (NoSuchMethodException e) {
			throw new NoSuchMethodError(e.getMessage());
		}
	}
	
	/**
	 * The generated domainFactory class should instantiate this LightSession, providing all arguments.
	 * @param remoteUrl
	 * @param clientVersion
	 * @param rootConceptClass
	 */
	public LightSession(URL serverContextUrl, Class rootConceptClass) {
		DomainProxyImpl.init();
		this.rootConceptClass = rootConceptClass;
		this.serverContextUrl = serverContextUrl;
		service = serviceFactory.createService(getServiceURL());
		verifyClientVersion();
	}

	private URL getServiceURL() {
		URL serviceUrl = null;
		try {
			serviceUrl = new URL(serverContextUrl.toExternalForm() + SERVICE_SUB_URL);
		} catch (MalformedURLException e) {
			throw new IllegalArgumentException("Error creating service-url" + serverContextUrl + SERVICE_SUB_URL, e); 
		}
		return serviceUrl;
	}
	
	
	/**
	 * Please notice that the return result is an Object array of HashMaps, while it really should be an Object array containing a single element: RootConceptImpl 
	 */
	private void registerRootConcept() {
		rootSystems = new HashMap<Class, DomainProxy>();
		Object system = service.retrieveRootSystem(initLinkDepth,rootConceptClass);
		rootSystems.put(system.getClass().getInterfaces()[0], DomainProxyImpl.newInstance(system, this, getDbIdMethod, initLinkDepth));			
	}
	
	private synchronized Map<Class, DomainProxy> getRootSystems(){
		if (null == rootSystems) {
			registerRootConcept();
		}
		return rootSystems;
	}
	
	/**
	 * @return Returns the remoteUrl.
	 */
	public URL getServerContextUrl() {
		return serverContextUrl;
	}
	
	/**
	 * @return Returns the rootProxy.
	 */
	@SuppressWarnings("unchecked")
	public <T> T getRootSystem() {
		return (T)getRootSystems().get(rootConceptClass);
	}
	
	/**
	 * Saves all new or modified concepts.
	 */
	@SuppressWarnings("unchecked")
	public synchronized void save() {
		Map<DispatchedModifyingMethodsTransport, DomainProxy> dispatchedModifyingMethods = new Hashtable<DispatchedModifyingMethodsTransport, DomainProxy>();
//		this.conceptVisitedMark = ! conceptVisitedMark;
//		root.getDispatchedModifyingMethods(dispatchedModifyingMethods, root, conceptVisitedMark);
		HashSet<DomainProxy> backTrace = new HashSet<DomainProxy>();
		for (DomainProxy rootSystem : getRootSystems().values()) {
			rootSystem.getDispatchedModifyingMethods(dispatchedModifyingMethods, rootSystem, backTrace);
		}
//		System.out.println("Found " + dispatchedModifyingMethods.size() + " dirty concepts");
		if (0 < dispatchedModifyingMethods.size()) {
			int i = 0;
			DispatchedModifyingMethodsTransport[] dispatchedModifyingMethodsArray = new DispatchedModifyingMethodsTransport[dispatchedModifyingMethods.size()];
			DomainProxy[] proxies = new DomainProxy[dispatchedModifyingMethods.size()];
			for (Entry<DispatchedModifyingMethodsTransport, DomainProxy> entry : dispatchedModifyingMethods.entrySet()) {
				dispatchedModifyingMethodsArray[i] = entry.getKey();
				proxies[i] = entry.getValue();
				i++;
			}
			Object[] savedConcepts = service.save(dispatchedModifyingMethodsArray);
			for (int j = 0 ; j < dispatchedModifyingMethodsArray.length ; j++ ) {
				proxies[j].updateFromPersistentStore(savedConcepts[j], proxies[j]);
			}
			for (DomainProxy proxy : proxies) {
				proxy.cleanUpAfterSave(proxy);
			}
		} else if (isAnyDirty) {
			log.finer("Session was marked dirty, but no dirty concepts were found. This is probably caused by inverse operations.");	
		}
		isAnyDirty = false;
	}
	
	public boolean isDirty(Object persistent) {
		if (persistent instanceof DomainProxy) {
			return ((DomainProxy)persistent).isDirty();
		}
		throw new DomainSessionException("Object was not proxy!");
	}
	
	public boolean isAnyDirty() {
		return isAnyDirty;
	}
	
	private void verifyClientVersion() {
		String[] serverCompatibleClientVersions = service.retrieveClientVersions();
		for (String version : serverCompatibleClientVersions) {
			if (CLIENT_VERSION.equals(version)) {
				return;
			}
		}
		throw new IncompatibleClientVersionException(serverCompatibleClientVersions, CLIENT_VERSION);
	}
	
	/**
	 * Initializes a new domain model concept.
	 * 
	 * @see com.wgo.bpot.common.transport.util.DomainModelTypeNamingConvention
	 * 
	 * Examples:
	 * Project project = createConcept(Project.class);
	 * Category category = createConcept(Category.class);
	 * Requirement requirement = createConcept(Requirement.class);
	 * Stakeholder stakeholder = createConcept(Stakeholder.class);
	 * PropertySet project = createConcept(PropertySet.class);
	 * PropertyDef propertyDef = createConcept(PropertyDef.class, PropertyType.TYPES[1]);
	 * PropertyValue propertyValue = createConcept(PropertyValue.class, propertyDef);
	 * 
	 * @param <T> The type of the new concept
	 * @param clazz The class of the instance. Note this has to be a known class
	 * 				located in the domain model package {@link com.wgo.bpot.common.transport.util.DomainModelTypeNamingConvention}
	 * @param args Possible types to be set on the object. Should be of concept types. 
	 * @return The new concept instance
	 */
	@SuppressWarnings("unchecked")
	public <T> T createConcept(Class<T> clazz, Object... args) {
		T actualConcept = null;
		Class<T> implClass = DomainModelTypeNamingConvention.loadImplClass(clazz);//, DomainModelTypeNamingConvention.getModelImplementationPackage(), DomainModelTypeNamingConvention.MODEL_IMPLEMENTATION_TYPE_SUFFIX);
		Map<Method,Object> methods = new Hashtable<Method,Object>();
		for (Object arg : args) {
			Class argClass = null;
			try { // EXTENSION: could be extended to handle more types of arguments.
				argClass = arg.getClass();
				while (argClass.isAnonymousClass()) { // could probably change from 'while' to 'if'
					argClass = argClass.getSuperclass();
				}
				methods.put(MethodNamingConvention.retrieveMethod("", MethodNamingConvention.SET_METHOD.getPrefix() + argClass.getInterfaces()[0].getSimpleName(), implClass, argClass.getInterfaces()[0]), arg);
			} catch (Exception e) {
				log.warning("Could not find method to set argument " + arg + " type: " + argClass.getInterfaces()[0].getSimpleName());
			}
		}
		try {
			if (0 < args.length) { // does not trigger fields to be updated at the server.. (but does verify that such parameter is legal to set)
				Class argClass = args[0].getClass();
				while (argClass.isAnonymousClass()) { // could probably change from 'while' to 'if'
					argClass = argClass.getSuperclass();
				}
				Constructor<T> constructor = implClass.getConstructor(argClass.getInterfaces()[0]);
				actualConcept = constructor.newInstance(new Object[]{args[0]});
			} else {
				actualConcept = implClass.newInstance();	
			}
		} catch (Exception e) {
			throw new DomainSessionException("Could not instantiate concept of type: " + implClass, e);
		}
		T proxy = (T) DomainProxyImpl.<T>newInstance(actualConcept, this, getDbIdMethod, 0);
		for (Entry<Method,Object> entry : methods.entrySet()) {
			((DomainProxy)proxy).putDirtyMethod(entry.getKey(), entry.getValue(), MethodNamingConvention.getMethodType(entry.getKey()).retrieveDefaultReturnValue(entry.getKey(), entry.getValue()));
		}
		return proxy;
	}
	
	protected void markDirty() {
		isAnyDirty = true;
	}
	
	protected LightService getService() {
		return service;
	}

	/**
	 * Only made available for testing purposes. This should ideally be factored out of
	 * this class. It would be a disaster if clients started to recreate the DB schema whenever they please...!
	 */
	public void createDbSchema() throws DomainSessionException {
		try {
			service.createDbSchema(rootConceptClass);
		} catch (DatabaseException e) {
			throw new DomainSessionException(e);
		}
	}

	public Class getRootConceptClass() {
		return rootConceptClass;
	}
}
